/**
 * This file is part of WiiuseJ.
 * 
 * WiiuseJ is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * WiiuseJ is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * WiiuseJ. If not, see <http://www.gnu.org/licenses/>.
 */
package wiiusej;

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.swing.event.EventListenerList;

import wiiusej.wiiusejevents.utils.EventsGatherer;
import wiiusej.wiiusejevents.utils.WiiUseApiListener;
import wiiusej.wiiusejevents.wiiuseapievents.WiiUseApiEvent;

/**
 * Class that manages the use of Wiiuse API.
 * 
 * @author guiguito
 */
public class WiiUseApiManager extends Thread
{

    /**
     * WIIUSE_STACK_BLUESOLEIL
     */
    public static int WIIUSE_STACK_BLUESOLEIL = 2;

    /**
     * WIIUSE_STACK_MS
     */
    public static int WIIUSE_STACK_MS = 1;

    /**
     * WIIUSE_STACK_UNKNOWN
     */
    public static int WIIUSE_STACK_UNKNOWN = 0;

    private static WiiUseApiManager instance = new WiiUseApiManager();

    /**
     * Stop wiiuseJ definitively for this program. It finishes Wiiusej thread
     * and shutdown wiiuse API.
     */
    public static void definitiveShutdown()
    {
        getInstance().leave = true;
        shutdown();
    }

    /**
     * @return instance
     */
    public static WiiUseApiManager getInstance()
    {
        return instance;
    }

    /**
     * Get the number of wiimotes connected.
     * 
     * @return the number of wiimotes connected.
     */
    public static int getNbConnectedWiimotes()
    {
        return getInstance().connected;
    }

    /**
     * Get wiimotes. Load library if necessary. Connect to wiimotes if
     * necessary. Start polling if necessary. Return an array with the connected
     * wiimotes.
     * 
     * @param nb
     *            try to connect nb wiimotes.
     * @param rumble
     *            make the connected wiimotes rumble.
     * 
     * @return an array with connected wiimotes or NULL.
     */
    public static Wiimote[] getWiimotes(final int nb,
            final boolean rumble)
    {
        return getWiimotesPrivate(nb, rumble, false, WIIUSE_STACK_UNKNOWN);
    }

    /**
     * Get wiimotes. Load library if necessary. Connect to wiimotes if
     * necessary. Start polling if necessary. Return an array with the connected
     * wiimotes.
     * 
     * @param nb
     *            try to connect nb wiimotes.
     * @param rumble
     *            make the connected wiimotes rumble.*
     * @param stackType
     *            the stack type : WiiUseApiManager.WIIUSE_STACK_UNKNOWN or
     *            WiiUseApiManager.WIIUSE_STACK_MS or
     *            WiiUseApiManager.WIIUSE_STACK_BLUESOLEIL
     * 
     * @return an array with connected wiimotes or NULL.
     */
    public static Wiimote[] getWiimotes(final int nb,
            final boolean rumble,
            final int stackType)
    {
        return getWiimotesPrivate(nb, rumble, true, stackType);
    }

    /**
     * Stop thread and shutdown wiiuse Api.
     */
    public static void shutdown()
    {
        final WiiUseApiManager manager = getInstance();
        final int pastConnected = manager.connected;
        if (manager.connected > 0)
        {
            for (final Wiimote wim : manager.wiimotes)
            {
                if (wim != null)
                {
                    wim.disconnect();
                }
            }
        }
        manager.running.set(false);
        if (pastConnected > 0)
        {
            manager.wiiuse.cleanUp();
        }
    }

    /**
     * Get wiimotes. Load library if necessary. Connect to wiimotes if
     * necessary. Start polling if necessary. Return an array with the connected
     * wiimotes.
     * 
     * @param nb
     *            try to connect nb wiimotes.
     * @param rumble
     *            make the connected wiimotes rumble.*
     * @param forceStackType
     *            true if we want to force the stack type.
     * @param stackType
     *            the stack type : WiiUseApiManager.WIIUSE_STACK_UNKNOWN or
     *            WiiUseApiManager.WIIUSE_STACK_MS or
     *            WiiUseApiManager.WIIUSE_STACK_BLUESOLEIL
     * 
     * @return an array with connected wiimotes or NULL.
     */
    private synchronized static Wiimote[] getWiimotesPrivate(final int nb,
            final boolean rumble,
            final boolean forceStackType,
            final int stackType)
    {
        final WiiUseApiManager manager = getInstance();

        if (manager.leave)
        {
            return null;// wiiusej definitively stopped
        }

        if (manager.connected <= 0 && !manager.running.get())
        {
            // connect wiimotes.
            final int nbWiimotes = manager.connectWiimotes(nb, rumble,
                    forceStackType, stackType);
            manager.wiimotes = new Wiimote[nbWiimotes];
            for (int i = 0; i < nbWiimotes; i++)
            {
                final Wiimote wim = new Wiimote(WiiUseApi.getInstance()
                    .getUnId(i), manager);
                manager.wiimotes[i] = wim;
                manager.addWiiUseApiListener(wim);
            }
            // Set leds on wiimote
            for (final Wiimote wiimote : manager.wiimotes)
            {
                final int id = wiimote.getId();
                if (id % 4 == 0)
                {
                    wiimote.setLeds(true, true, true, true);
                }
                else if (id % 4 == 1)
                {
                    wiimote.setLeds(true, false, false, false);
                }
                else if (id % 4 == 2)
                {
                    wiimote.setLeds(true, true, false, false);
                }
                else if (id % 4 == 3)
                {
                    wiimote.setLeds(true, true, true, false);
                }
            }
            // make the connected wiimotes rumble
            if (rumble)
            {
                for (final Wiimote wiimote : manager.wiimotes)
                {
                    wiimote.activateRumble();
                }
                try
                {
                    sleep(500);
                }
                catch (final InterruptedException e)
                {
                }
                for (final Wiimote wiimote : manager.wiimotes)
                {
                    wiimote.deactivateRumble();
                }
            }
        }

        if (manager.connected == 0)
        {// no wiimote connected
            // return empty array
            return new Wiimote[0];
        }

        if (!manager.isAlive())
        {
            manager.start();
        }

        manager.semaphore.release(1);

        return manager.wiimotes;
    }

    @Override
    public void run()
    {

        while (!leave)
        {
            try
            {
                semaphore.acquire();
            }
            catch (final InterruptedException e)
            {
                e.printStackTrace();
            }

            if (connected > 0)
            {
                running.set(true);

                final EventsGatherer gather = new EventsGatherer(connected);

                // Start polling and tell the observers when there are Wiimote
                // events
                while (running.get() && connected > 0)
                {

                    /* Polling */
                    wiiuse.specialPoll(gather);

                    /* deal with events gathered in Wiiuse API */
                    for (final WiiUseApiEvent evt : gather.getEvents())
                    {
                        if (evt.getWiimoteId() != -1)
                        {// event filled
                            // there is an event notify observers
                            notifyWiiUseApiListener(evt);
                            if (evt.getEventType() == WiiUseApiEvent.DISCONNECTION_EVENT)
                            {
                                // check if it was a disconnection
                                // in this case disconnect the wiimote
                                closeConnection(evt.getWiimoteId());
                            }
                        }
                        else
                        {
                            System.out.println("There is an event with id == -1 ??? there is a problem !!! : "
                                    + evt);
                        }
                    }
                    gather.clearEvents();
                }
            }/*
              * else { if (connected <= 0) {
              * System.out.println("No wiimotes connected !"); } }
              */
        }// end while true
    }

    /**
     * Connect wiimote and get the number of wiimotes connected. Supposed to be
     * used once.
     * 
     * @param nb
     *            try to connect nb wiimotes
     * @param rumble
     *            make the connected wiimotes rumble
     * @param forceStackType
     *            true if we want to force the stack type.
     * @param stackType
     *            the stack type : WiiUseApiManager.WIIUSE_STACK_UNKNOWN or
     *            WiiUseApiManager.WIIUSE_STACK_MS or
     *            WiiUseApiManager.WIIUSE_STACK_BLUESOLEIL
     * @return 0 if nothing connected or the number of wiimotes connected.
     */
    private int connectWiimotes(final int nb,
            final boolean rumble,
            final boolean forceStackType,
            final int stackType)
    {
        if (connected <= 0)
        {
            int nbWiimotesFound;
            wiiuse.init(nb);
            // force bluetooth stack type ?
            if (forceStackType)
            {
                setBlueToothstackType(stackType);
            }
            nbWiimotesFound = wiiuse.find(nb, 3);
            connected = wiiuse.connect(nbWiimotesFound);
            return connected;
        }
        else
        {// library not loaded, no wiimotes connected
            return 0;
        }
    }

    /**
     * Notify WiiUseApiListeners that an event occured.
     * 
     * @param evt
     *            GenericEvent occured
     */
    private void notifyWiiUseApiListener(final WiiUseApiEvent evt)
    {
        for (final WiiUseApiListener listener : getWiiUseApiListeners())
        {
            listener.onWiiUseApiEvent(evt);
        }
    }

    /**
     * Force the bluetooth stack type.(useful only for windows)
     * 
     * @param type
     *            must be WIIUSE_STACK_UNKNOWN or WIIUSE_STACK_MS or
     *            WIIUSE_STACK_BLUESOLEIL.
     */
    private void setBlueToothstackType(final int type)
    {
        wiiuse.windowsSetBluetoothStack(type);
    }

    /**
     * Activate continuous for the wiimotes with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void activateContinuous(final int id)
    {
        wiiuse.activateContinuous(id);
    }

    /**
     * Activate IR Tracking for the wiimote with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void activateIRTRacking(final int id)
    {
        wiiuse.activateIRTracking(id);
    }

    /**
     * Activate motion sensing for the wiimote with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void activateMotionSensing(final int id)
    {
        wiiuse.activateMotionSensing(id);
    }

    /**
     * Activate the rumble for the wiimote with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void activateRumble(final int id)
    {
        wiiuse.activateRumble(id);
    }

    /**
     * Activate smoothing the wiimotes with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void activateSmoothing(final int id)
    {
        wiiuse.activateSmoothing(id);
    }

    /**
     * Add WiiUseApiListener to the listeners list.
     * 
     * @param listener
     *            a WiiUseApiListener
     */
    protected void addWiiUseApiListener(final WiiUseApiListener listener)
    {
        listeners.add(WiiUseApiListener.class, listener);
    }

    /**
     * Ask the thread to close a connection.
     * 
     * @param id
     *            id of the wiimote to disconnect.
     */
    protected void closeConnection(final int id)
    {
        int index = 0;
        final boolean found = false;
        while (index < wiimotes.length && !found)
        {
            if (wiimotes[index] != null && wiimotes[index].getId() == id)
            {// we have a wiimote with this
                // id
                // remove the wiimote
                removeWiiUseApiListener(wiimotes[index]);
                wiimotes[index] = null;
                connected--;
                if (connected == 0)
                {// stop this thread if there is
                    // no more wiimotes connected.
                    // stop thread
                    running.set(false);
                }
                /* Close connection in wiiuse */
                wiiuse.closeConnection(index);
            }
            index++;
        }
    }

    /**
     * Deactivate continuous for the wiimotes with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void deactivateContinuous(final int id)
    {
        wiiuse.deactivateContinuous(id);
    }

    /**
     * Deactivate IR Tracking for the wiimote with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void deactivateIRTRacking(final int id)
    {
        wiiuse.deactivateIRTracking(id);
    }

    /**
     * Deactivate motion sensing for the wiimoter with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void deactivateMotionSensing(final int id)
    {
        wiiuse.deactivateMotionSensing(id);
    }

    /**
     * Deactivate the rumble for the wiimote with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void deactivateRumble(final int id)
    {
        wiiuse.deactivateRumble(id);
    }

    /**
     * Deactivate smoothing the wiimotes with the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void deactivateSmoothing(final int id)
    {
        wiiuse.deactivateSmoothing(id);
    }

    /**
     * Called by the garbage collector at the end.
     */
    @Override
    protected void finalize() throws Throwable
    {
        shutdown();
    }

    /**
     * Get Status for the wiimote for the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void getStatus(final int id)
    {
        wiiuse.getStatus(id);
    }

    /**
     * Get the list of WiiUseApiListeners.
     * 
     * @return the list of WiiUseApiListeners.
     */
    protected WiiUseApiListener[] getWiiUseApiListeners()
    {
        return listeners.getListeners(WiiUseApiListener.class);
    }

    /**
     * Remove WiiUseApiListener from the listeners list.
     * 
     * @param listener
     *            a WiiUseApiListener
     */
    protected void removeWiiUseApiListener(final WiiUseApiListener listener)
    {
        listeners.remove(WiiUseApiListener.class, listener);
    }

    /**
     * Try to resync with the wiimote by starting a new handshake.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void reSync(final int id)
    {
        wiiuse.reSync(id);
    }

    /**
     * Set the acceleration threshold for the given id. (minimum angle between
     * two events)
     * 
     * @param id
     *            id of the wiimote.
     * @param th
     *            threshold.
     */
    protected void setAccelerationThreshold(final int id,
            final int th)
    {
        wiiuse.setAccelThreshold(id, th);
    }

    /**
     * Set alpha smoothing for the given id.
     * 
     * @param id
     *            id of the wiimote.
     * @param th
     *            threshold.
     */
    protected void setAlphaSmoothing(final int id,
            final float th)
    {
        wiiuse.setAlphaSmoothing(id, th);
    }

    /**
     * Set the IR sensitivity.
     * 
     * @param id
     *            the id of the wiimote concerned.
     * @param level
     *            1-5, same as Wii system sensitivity setting. If the level is <
     *            1, then level will be set to 1. If the level is > 5, then
     *            level will be set to 5.
     */
    protected void setIrSensitivity(final int id,
            final int level)
    {
        wiiuse.setIrSensitivity(id, level);
    }

    /**
     * Set leds for the wiimotes with the given id.
     * 
     * @param id
     *            id of the wiimote
     * @param l1
     *            status of led1. True : ON, False : OFF.
     * @param l2
     *            status of led2. True : ON, False : OFF.
     * @param l3
     *            status of led3. True : ON, False : OFF.
     * @param l4
     *            status of led4. True : ON, False : OFF.
     */
    protected void setLeds(final int id,
            final boolean l1,
            final boolean l2,
            final boolean l3,
            final boolean l4)
    {
        wiiuse.setLeds(id, l1, l2, l3, l4);
    }

    /**
     * Set the nunchuk acceleration threshold for the given id. (minimum angle
     * between two events)
     * 
     * @param id
     *            id of the wiimote.
     * @param th
     *            threshold.
     */
    protected void setNunchukAccelerationThreshold(final int id,
            final int th)
    {
        wiiuse.setNunchukAccelerationThreshold(id, th);
    }

    /**
     * Set the nunchuk orientation threshold for the given id. (minimum angle
     * between two events)
     * 
     * @param id
     *            id of the wiimote.
     * @param th
     *            threshold in degrees.
     */
    protected void setNunchukOrientationThreshold(final int id,
            final float th)
    {
        wiiuse.setNunchukOrientationThreshold(id, th);
    }

    /**
     * Set the orientation threshold for the given id. (minimum angle between
     * two events)
     * 
     * @param id
     *            id of the wiimote.
     * @param th
     *            threshold in degrees.
     */
    protected void setOrientationThreshold(final int id,
            final float th)
    {
        wiiuse.setOrientThreshold(id, th);
    }

    /**
     * Set screen aspect ratio to 16/9 for the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void setScreenAspectRatio169(final int id)
    {
        wiiuse.setScreenRatio169(id);
    }

    /**
     * Set screen aspect ratio to 4/3 for the given id.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void setScreenAspectRatio43(final int id)
    {
        wiiuse.setScreenRatio43(id);
    }

    /**
     * Set the sensor bar to be above the screen.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void setSensorBarAboveScreen(final int id)
    {
        wiiuse.setSensorBarAboveScreen(id);
    }

    /**
     * Set the sensor bar to be below the screen.
     * 
     * @param id
     *            id of the wiimote.
     */
    protected void setSensorBarBelowScreen(final int id)
    {
        wiiuse.setSensorBarBelowScreen(id);
    }

    /**
     * Set the normal and expansion handshake timeouts.
     * 
     * @param id
     *            the id of the wiimote concerned.
     * @param normalTimeout
     *            The timeout in milliseconds for a normal read.
     * @param expansionTimeout
     *            The timeout in millisecondsd to wait for an expansion
     *            handshake.
     */
    protected void setTimeout(final int id,
            final short normalTimeout,
            final short expansionTimeout)
    {
        wiiuse.setTimeout(id, normalTimeout, expansionTimeout);
    }

    /**
     * Set virtual resolution. It is used to automatically compute the position
     * of a cursor on this virtual screen using the sensor bar. These results
     * come in the IREvent.
     * 
     * @param id
     *            id of the wiimote.
     * @param x
     *            x resolution.
     * @param y
     *            y resolution.
     */
    protected void setVirtualResolution(final int id,
            final int x,
            final int y)
    {
        wiiuse.setVirtualScreenResolution(id, x, y);
    }

    private int connected = -1;

    private boolean leave = false;

    private final EventListenerList listeners = new EventListenerList();

    private final AtomicBoolean running = new AtomicBoolean(false);

    private final Semaphore semaphore = new Semaphore(0);

    private Wiimote[] wiimotes;

    private final WiiUseApi wiiuse = WiiUseApi.getInstance();

}
